/*
 * Copyright 2025 author or author.
 * SPDX-License-Identifier: Apache-2.0
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.jcwang.commons.tools.util.net;

import com.jcwang.commons.tools.util.net.client.HttpServletRequestHeaderAdapter;
import com.jcwang.commons.tools.util.net.client.RequestHeaderProvider;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 获取客户端信息工具类
 *
 * @author 王金城
 **/
public class ClientInfoUtils {

    private static final String USER_AGENT = "User-Agent";
    private static final String X_REAL_IP = "X-Real-IP";
    private static final String X_FORWARDED_FOR = "X-Forwarded-For";
    private static final String UN_KNOWN = "unKnown";
    private static final String LOCALHOST_IPV4 = "127.0.0.1";
    private static final String LOCALHOST_IPV6 = "0:0:0:0:0:0:0:1";

    /**
     * 获取浏览器版本正则
     **/
    private static final String COMMON_VERSION_REGEX = "version/(\\d+(\\.?_?\\d+)+)";
    /**
     * 浏览器通用版本正则
     **/
    private static final Pattern COMMON_PATTERN = Pattern.compile(COMMON_VERSION_REGEX, Pattern.CASE_INSENSITIVE);

    /**
     * 浏览器名称与版本获取正则
     */
    private static final BrowserRegex[] BROWSER_REGEXES = new BrowserRegex[]{
            new BrowserRegex("UC Browser", ".*ucbrowser.*", "ucbrowser[\\s/](\\d+(\\.?_?\\d+)+)"),
            new BrowserRegex("360 Browser", ".*QIHU\\s360se.*", "chrome[\\s/](\\d+(\\.?_?\\d+)+)"),
            new BrowserRegex("360EE Browser", ".*QIHU\\s360ee.*", "chrome[\\s/](\\d+(\\.?_?\\d+)+)"),
            new BrowserRegex("WeChat", ".*micromessenger.*", "micromessenger[\\s/](\\d+(\\.?_?\\d+)+)"),
            new BrowserRegex("QQ Browser", ".*qqbrowser.*", "(?:qqbrowserlite|qqbrowser)[/](\\d+(\\.?_?\\d+)+)"),
            new BrowserRegex("Internet Explorer", ".*msie.*|.*trident.*", "(?:msie |rv:)(\\d+(\\.?_?\\d+)+)"),
            new BrowserRegex("Microsoft Edge Legacy", ".*edge.*", "edge/(\\d+(\\.?_?\\d+)+)"),
            new BrowserRegex("Microsoft Edge", ".*edg.*", "edg/(\\d+(\\.?_?\\d+)+)"),
            new BrowserRegex("Microsoft Edge for Android", ".*EdgA.*", "EdgA/(\\d+(\\.?_?\\d+)+)"),
            new BrowserRegex("Firefox", ".*firefox.*|.*iceweasel.*|.*fxios.*", "(?:firefox|iceweasel|fxios)[\\s/](\\d+(\\.?_?\\d+)+)"),
            new BrowserRegex("Quark", ".*Quark.*", "Quark[\\s/](\\d+(\\.?_?\\d+)+)"),
            new BrowserRegex("HuaweiBrowser", ".*HuaweiBrowser.*", "HuaweiBrowser[\\s/](\\d+(\\.?_?\\d+)+)"),
            new BrowserRegex("MiuiBrowser", ".*MiuiBrowser.*", "MiuiBrowser[\\s/](\\d+(\\.?_?\\d+)+)"),
            new BrowserRegex("Maxthon", ".*maxthon.*|.*mxios.*", "(?:Maxthon|mxios)[\\s/](\\d+(\\.?_?\\d+)+)"),
            new BrowserRegex("Opera", ".*opera.*", "opera[\\s/](\\d+(\\.?_?\\d+)+)"),
            new BrowserRegex("Opera", ".*opr/.*|.*opios.*", "(?:opr|opios)[\\s/](\\S+)"),
            new BrowserRegex("Samsung Internet for Android", ".*SamsungBrowser.*", "SamsungBrowser[\\s/](\\d+(\\.?_?\\d+)+)"),
            new BrowserRegex("MZ Browser", ".*MZBrowser.*", "MZBrowser[\\s/](\\d+(?:\\.\\d+)+)"),
            new BrowserRegex("Opera Coast", ".*coast.*", "coast[\\s/](\\d+(\\.?_?\\d+)+)"),
            new BrowserRegex("Baiduspider", ".*Baiduspider.*", "Baiduspider/(\\d+(\\.\\d+))"),
            new BrowserRegex("Googlebot", ".*googlebot.*", "googlebot/(\\d+(\\.\\d+))"),
            new BrowserRegex("Google Chrome", ".*chrome.*|.*crios.*|.*crmo.*", "(?:chrome|crios|crmo)/(\\d+(\\.?_?\\d+)+)"),
            new BrowserRegex("Google Search", ".*GSA.*", "GSA/(\\d+(\\.?_?\\d+)+)"),
            new BrowserRegex("Safari", ".*safari.*|.*applewebkit.*", "version/(\\d+(\\.?_?\\d+)+)")
    };

    /**
     * 操作系统名称与判断正则
     **/
    private static final OSRegex[] OS_REGEXES = new OSRegex[]{
            new OSRegex("Windows 10", ".*windows\\snt\\s10\\.0.*"),
            new OSRegex("Windows 8.1", ".*windows\\snt\\s6\\.3.*"),
            new OSRegex("Windows 7", ".*windows\\snt\\s6\\.1.*"),
            new OSRegex("Windows 8", ".*windows\\snt\\s6\\.2.*"),
            new OSRegex("Windows XP", ".*windows\\snt\\s5\\.1.*"),
            new OSRegex("Windows 98", ".*windows\\s98.*"),
            new OSRegex("Windows ME", ".*windows\\sme.*"),
            new OSRegex("Windows 2000", ".*windows\\snt\\s5\\.0.*"),
            new OSRegex("Windows 2003", ".*windows\\snt\\s5\\.2.*"),
            new OSRegex("Windows Vista", ".*windows\\snt\\s6\\.0.*"),
            new OSRegex("Windows", ".*windows.*"),
            new OSRegex("Mac OS", ".*macintosh.*"),
            new OSRegex("Android", ".*android.*"),
            new OSRegex("IOS", ".*ipod.*|.*iphone.*|.*ipad.*"),
            new OSRegex("Linux", ".*linux.*"),
            new OSRegex("Chrome OS", ".*CrOS.*"),
            new OSRegex("Windows Phone", ".*windows\\sphone.*")
    };

    private static final Logger logger = LoggerFactory.getLogger(ClientInfoUtils.class);

    /**
     * 获取客户端IP地址，想要正确获取到客户端IP地址，需要配置 Nginx 的代理服务器，并且需要设置变量 $proxy_add_x_forwarded_for
     *
     * @param request HttpServletRequest
     * @return java.lang.String IP地址 默认为 ""
     * @author 王金城
     */
    public static String getClientIp(HttpServletRequest request) {
        if (request == null) {
            return "";
        }
        return getClientIp(HttpServletRequestHeaderAdapter.of(request));
    }

    /**
     * 获取客户端IP地址，这个工具类一般用于网关中，想要正确获取到客户端IP地址，需要配置 Nginx 的代理服务器，并且需要设置变量 $proxy_add_x_forwarded_for
     *
     * @param requestHeaderProvider 客户端请求获取 header 提供，使用 {@link RequestHeaderProvider} 的实现类即可
     * @return {@code java.lang.String}
     * @author 王金城
     */
    public static String getClientIp(RequestHeaderProvider requestHeaderProvider) {
        if (requestHeaderProvider == null) {
            return "";
        }
        // 优先获取 X-Forwarded-For 中的IP地址，如果获取到那就以此为准
        String ip = ClientIpUtils.xForwardedForHandler(requestHeaderProvider.getHeader(X_FORWARDED_FOR));
        if (ClientIpUtils.isIp(ip)) {
            return ip;
        }
        // 其次获取 X-Real-IP 中的IP地址，如果获取到那就以此为准
        ip = ClientIpUtils.xRealIpHandler(requestHeaderProvider.getHeader(X_REAL_IP));
        if (ClientIpUtils.isIp(ip)) {
            return ip;
        }
        // 这是无可奈何后使用的IP
        ip = requestHeaderProvider.getRemoteAddr();
        // 获取到的本地地址，则代表为本机，故获取网卡地址为访问地址
        if (ClientIpUtils.isLocalhost(ip)) {
            ip = ServerInfoUtils.getServerIp();
        }
        return ip;
    }

    /**
     * 获取UserAgent
     *
     * @param request HttpServletRequest
     * @return java.lang.String
     * @author 王金城
     */
    public static String getUserAgent(HttpServletRequest request) {
        return request.getHeader(USER_AGENT);
    }

    /**
     * 获取浏览器名称
     *
     * @param request HttpServletRequest
     * @return java.lang.String 返回浏览器名称。默认为 ""
     * @author 王金城
     */
    public static String getBrowserName(HttpServletRequest request) {
        try {
            String ua = request.getHeader(USER_AGENT);
            if (StringUtils.isBlank(ua)) {
                return "";
            }
            for (BrowserRegex browserRegex : BROWSER_REGEXES) {
                Pattern r = Pattern.compile(browserRegex.getRegex(), Pattern.CASE_INSENSITIVE);
                Matcher m = r.matcher(ua);
                boolean matches = m.matches();
                if (matches) {
                    return browserRegex.getName();
                }
            }
        } catch (Exception e) {
            logger.error("获取浏览器名称异常", e);
        }
        return "";
    }

    /**
     * 获取浏览器版本
     *
     * @param request HttpServletRequest
     * @return java.lang.String 返回浏览器版本。默认为 ""
     * @author 王金城
     */
    public static String getBrowserVersion(HttpServletRequest request) {
        try {
            String ua = request.getHeader(USER_AGENT);
            if (StringUtils.isBlank(ua)) {
                return "";
            }
            for (BrowserRegex browserRegex : BROWSER_REGEXES) {
                Pattern r = browserRegex.getPattern();
                Matcher m = r.matcher(ua);
                boolean matches = m.matches();
                if (!matches) {
                    continue;
                }
                Pattern versionPattern = browserRegex.getVersionPattern();
                Matcher matcher = versionPattern.matcher(ua);
                boolean find = matcher.find();
                if (find && matcher.groupCount() >= 2) {
                    return matcher.group(1);
                }
                // 如果没有获取到版本，尝试用通用模式获取版本
                Matcher commonMatcher = COMMON_PATTERN.matcher(ua);
                boolean commonFind = commonMatcher.find();
                if (commonFind && commonMatcher.groupCount() >= 2) {
                    return commonMatcher.group(1);
                }
            }
        } catch (Exception e) {
            logger.error("获取浏览器版本异常", e);
        }
        return "";
    }

    /**
     * 获取操作系统
     *
     * @param request HttpServletRequest
     * @return java.lang.String 返回操作系统名称。默认为 ""
     * @author 王金城
     */
    public static String getPlatform(HttpServletRequest request) {
        try {
            String ua = request.getHeader(USER_AGENT);
            if (StringUtils.isBlank(ua)) {
                return "";
            }
            for (OSRegex osRegex : OS_REGEXES) {
                Pattern r = osRegex.getPattern();
                Matcher m = r.matcher(ua);
                boolean matches = m.matches();
                if (matches) {
                    return osRegex.getName();
                }
            }
        } catch (Exception e) {
            logger.error("获取操作系统异常", e);
        }
        return "";
    }

    /**
     * 获取浏览器信息，不包含系统信息
     *
     * @param request HttpServletRequest
     * @return 返回浏览器的信息，包含名字、版本、操作系统的对象。如果没有获取到则为空对象，内部属性值将为null
     * @author 王金城
     */
    public static BrowserInfo getBrowserInfoExcludeOS(HttpServletRequest request) {
        return getBrowserInfo(request, false);
    }

    /**
     * 获取浏览器信息
     *
     * @param request HttpServletRequest
     * @param os      是否要获取操作系统名称
     * @return 返回浏览器的信息，包含名字、版本、操作系统的对象。如果没有获取到则为空对象，内部属性值将为null
     * @author 王金城
     */
    public static BrowserInfo getBrowserInfo(HttpServletRequest request, boolean os) {
        BrowserInfo browserInfo = new BrowserInfo();
        try {
            String ua = request.getHeader(USER_AGENT);
            if (StringUtils.isBlank(ua)) {
                return browserInfo;
            }
            for (BrowserRegex browserRegex : BROWSER_REGEXES) {
                Pattern r = Pattern.compile(browserRegex.getRegex(), Pattern.CASE_INSENSITIVE);
                Matcher m = r.matcher(ua);
                boolean matches = m.matches();
                if (!matches) {
                    continue;
                }
                browserInfo.setName(browserRegex.getName());
                Pattern pattern = Pattern.compile(browserRegex.getVersionRegex(), Pattern.CASE_INSENSITIVE);
                Matcher matcher = pattern.matcher(ua);
                boolean find = matcher.find();
                if (find && matcher.groupCount() >= 2) {
                    browserInfo.setVersion(matcher.group(1));
                    break;
                }
                // 如果没有获取到版本，尝试用通用模式获取版本
                Pattern commonPattern = Pattern.compile(COMMON_VERSION_REGEX, Pattern.CASE_INSENSITIVE);
                Matcher commonMatcher = commonPattern.matcher(ua);
                boolean commonFind = commonMatcher.find();
                if (commonFind && commonMatcher.groupCount() >= 2) {
                    browserInfo.setVersion(commonMatcher.group(1));
                    break;
                }
            }
            if (os) {
                browserInfo.setOs(getPlatform(request));
            }
        } catch (Exception e) {
            logger.error("获取浏览器信息异常", e);
        }
        return browserInfo;
    }


    /**
     * 当前类中获取客户端 ip 的工具类
     *
     * @author 王金城
     */
    private static class ClientIpUtils {

        /**
         * 处理从代理服务器过来的请求头中的 X-Forwarded-For 字段
         *
         * @param ip 请求头中的 X-Forwarded-For 字段
         * @return java.lang.String 如果没获取到就返回 null
         * @author 王金城
         */
        private static String xForwardedForHandler(String ip) {
            if (!isIp(ip)) {
                return null;
            }
            // 多次反向代理后会有多个ip值，第一个ip才是真实ip
            int index = ip.indexOf(",");
            if (index != -1) {
                ip = ip.substring(0, index);
            }
            // 获取到的本地地址，则代表为本机，故获取网卡地址为访问地址
            if (isLocalhost(ip)) {
                ip = ServerInfoUtils.getServerIp();
            }
            return ip;
        }

        /**
         * 处理从代理服务器过来的请求头中的 x-real-ip 字段
         *
         * @param ip 请求头中的 X-Real-IP 字段
         * @return java.lang.String 如果没获取到就返回 null
         * @author 王金城
         */
        private static String xRealIpHandler(String ip) {
            if (!isIp(ip)) {
                return null;
            }
            // 获取到的本地地址，则代表为本机，故获取网卡地址为访问地址
            if (isLocalhost(ip)) {
                ip = ServerInfoUtils.getServerIp();
            }
            return ip;
        }

        /**
         * 判断字符串是否为合法的 ip，这个方法只能在获取客户端 ip 的方法中使用
         *
         * @param ip 要判断的地址
         * @return boolean
         * @author 王金城
         */
        private static boolean isIp(String ip) {
            return StringUtils.isNotEmpty(ip) && !UN_KNOWN.equalsIgnoreCase(ip);
        }

        /**
         * 是否为本机地址
         *
         * @param ip 要判断的地址
         * @return boolean
         * @author 王金城
         */
        private static boolean isLocalhost(String ip) {
            return LOCALHOST_IPV6.equals(ip) || LOCALHOST_IPV4.equals(ip);
        }

    }

}
